Passed
Push — master ( ddaf3b...66a4f5 )
by Pawel
03:22
created

GraphHelpers.ts ➔ createSimulation   A

Complexity

Conditions 1

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 15
rs 9.8
c 0
b 0
f 0
cc 1
1
import { DependencyLink, DependencyNode } from '../../components/types';
2
import { select, Selection, event, BaseType } from 'd3-selection';
3
import { Simulation } from 'd3-force';
4
import { drag } from 'd3-drag';
5
import { zoom, zoomIdentity } from 'd3-zoom';
6
7
export type NodeSelection<T extends BaseType> = Selection<T, DependencyNode, Element, HTMLElement>;
8
9
export type LinkSelection = Selection<SVGPathElement, DependencyLink, SVGGElement, DependencyNode>;
10
11
export enum LabelColors {
12
    PROVIDER = '#00BFC2',
13
    CONSUMER = '#039881',
14
    PROVIDER_CONSUMER = '#03939F',
15
    DEFAULT = '#dcdee0',
16
}
17
18
export enum TextColors {
19
    HIGHLIGHTED = 'WHITE',
20
    DEFAULT = '#5E6063',
21
}
22
23
export function getLabelTextDimensions(node: Node) {
24
    const textNode = select<SVGGElement, DependencyNode>(node.previousSibling as SVGGElement).node();
25
26
    if (!textNode) {
27
        return undefined;
28
    }
29
30
    return textNode.getBBox();
31
}
32
33
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
34
    const foundNode = select<SVGGElement, DependencyNode>('#labels')
35
        .selectAll<SVGGElement, DependencyNode>('g')
36
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
37
        .node();
38
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
39
}
40
41
export function findMaxDependencyLevel(labelNodesGroup: NodeSelection<SVGGElement>) {
42
    return (
43
        Math.max(
44
            ...labelNodesGroup
45
                .selectAll<HTMLElement, DependencyNode>('g')
46
                .filter((node: DependencyNode) => node.level > 0)
47
                .data()
48
                .map((node: DependencyNode) => node.level)
49
        ) - 1
50
    );
51
}
52
53
export function highlight(clickedNode: DependencyNode, links: LinkSelection) {
54
    const linksData = links.data();
55
    const labelNodes = selectAllNodes();
56
57
    const visitedNodes = setDependencyLevelOnEachNode(clickedNode, labelNodes.data());
58
59
    if (visitedNodes.length === 1) {
60
        return;
61
    }
62
63
    labelNodes.each(function(this: SVGGElement, node: DependencyNode) {
64
        const areNodesDirectlyConnected = areNodesConnected(clickedNode, node, linksData);
65
        const labelElement = this.firstElementChild;
66
        const textElement = this.lastElementChild;
67
68
        if (!labelElement || !textElement) {
69
            return;
70
        }
71
72
        if (areNodesDirectlyConnected) {
73
            select<Element, DependencyNode>(labelElement).attr('fill', getHighLightedLabelColor);
74
            select<Element, DependencyNode>(textElement).style('fill', TextColors.HIGHLIGHTED);
75
        } else {
76
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
77
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
78
        }
79
    });
80
}
81
82
export function selectHighLightedNodes() {
83
    return selectAllNodes().filter(function(this: SVGGElement) {
84
        return this.firstElementChild ? this.firstElementChild.getAttribute('fill') !== LabelColors.DEFAULT : false;
85
    });
86
}
87
88
export function selectAllNodes() {
89
    return select('#labels').selectAll<SVGGElement, DependencyNode>('g');
90
}
91
92
export function zoomToHighLightedNodes() {
93
    const highlightedNodes = selectHighLightedNodes();
94
    const svgContainer = select('#container');
95
    const svgContainerNode = select<SVGSVGElement, DependencyNode>('#container').node();
96
    const dim = findGroupBackgroundDimension(highlightedNodes.data());
97
98
    if (!svgContainerNode || !dim) {
99
        return;
100
    }
101
102
    const width = Number(svgContainerNode.getAttribute('width'));
103
    const height = Number(svgContainerNode.getAttribute('height'));
104
105
    const scaleValue = Math.min(8, 0.9 / Math.max(dim.width / width, dim.height / height));
106
107
    svgContainer
108
        .transition()
109
        .duration(750)
110
        .call(
111
            zoom<any, any>().on('zoom', zoomed).transform,
112
            zoomIdentity
113
                .translate(width / 2, height / 2)
114
                .scale(scaleValue)
115
                .translate(-dim.x - dim.width / 2, -dim.y - dim.height / 2)
116
        );
117
}
118
119
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
120
    nodes.forEach((node: DependencyNode) => (node.level = 0));
121
122
    const visitedNodes: DependencyNode[] = [];
123
    const nodesToVisit: DependencyNode[] = [];
124
125
    nodesToVisit.push({ ...clickedNode, level: 1 });
126
127
    while (nodesToVisit.length > 0) {
128
        const currentNode = nodesToVisit.shift();
129
130
        if (!currentNode) {
131
            return [];
132
        }
133
134
        currentNode.links.forEach((node: DependencyNode) => {
135
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
136
                node.level = currentNode.level + 1;
137
                nodesToVisit.push(node);
138
            }
139
        });
140
141
        visitedNodes.push(currentNode);
142
    }
143
144
    return visitedNodes;
145
}
146
147
function containsNode(arr: DependencyNode[], node: DependencyNode) {
148
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
149
}
150
151
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
152
    node1: T,
153
    node2: K
154
): Boolean {
155
    return node1.name === node2.name && node1.version === node2.version;
156
}
157
158
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
159
    return (
160
        a.index === b.index ||
161
        links.some(
162
            link =>
163
                (link.source.index === a.index && link.target.index === b.index) ||
164
                (link.source.index === b.index && link.target.index === a.index)
165
        )
166
    );
167
}
168
169
export function getHighLightedLabelColor(node: DependencyNode) {
170
    const { isConsumer, isProvider } = node;
171
172
    if (isConsumer && isProvider) {
173
        return LabelColors.PROVIDER_CONSUMER;
174
    }
175
176
    if (isProvider) {
177
        return LabelColors.PROVIDER;
178
    }
179
180
    if (isConsumer) {
181
        return LabelColors.CONSUMER;
182
    }
183
184
    return LabelColors.DEFAULT;
185
}
186
187
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
188
    return drag<SVGGElement, DependencyNode>()
189
        .on('start', (node: DependencyNode) => dragStarted(node, simulation))
190
        .on('drag', dragged)
191
        .on('end', (node: DependencyNode) => dragEnded(node, simulation));
192
}
193
194
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
195
    if (!event.active) {
196
        simulation.alphaTarget(0.3).restart();
197
    }
198
    node.fx = node.x;
199
    node.fy = node.y;
200
}
201
202
function dragged(node: DependencyNode) {
203
    node.fx = event.x;
204
    node.fy = event.y;
205
}
206
207
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
208
    if (!event.active) {
209
        simulation.alphaTarget(0);
210
    }
211
    node.fx = null;
212
    node.fy = null;
213
}
214
215
export function zoomed() {
216
    const { transform } = event;
217
    const zoomLayer = select('#zoom');
218
    zoomLayer.attr('transform', transform);
219
    zoomLayer.attr('stroke-width', 1 / transform.k);
220
}
221
222
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
223
    if (nodesGroup.length === 0) {
224
        return undefined;
225
    }
226
227
    let upperLimitNode = nodesGroup[0];
228
    let lowerLimitNode = nodesGroup[0];
229
    let leftLimitNode = nodesGroup[0];
230
    let rightLimitNode = nodesGroup[0];
231
232
    nodesGroup.forEach((node: DependencyNode) => {
233
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
234
            return;
235
        }
236
        if (node.x > rightLimitNode.x) {
237
            rightLimitNode = node;
238
        }
239
240
        if (node.x < leftLimitNode.x) {
241
            leftLimitNode = node;
242
        }
243
244
        if (node.y < upperLimitNode.y) {
245
            upperLimitNode = node;
246
        }
247
248
        if (node.y > lowerLimitNode.y) {
249
            lowerLimitNode = node;
250
        }
251
    });
252
253
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
254
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
255
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
256
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
257
258
    return {
259
        x: leftLimitWithOffset,
260
        y: upperLimitWithOffset,
261
        width,
262
        height,
263
    };
264
}
265
266
export function setResetViewHandler() {
267
    LevelStorage.reset();
268
    const svgContainer = select('#container');
269
    const zoomLayer = select('#zoom');
270
    svgContainer.on('dblclick', () => {
271
        selectAllNodes().each((node: DependencyNode) => (node.level = 0));
272
273
        selectHighLightedNodes().each(function(this: SVGGElement) {
274
            const labelElement = this.firstElementChild;
275
            const textElement = this.lastElementChild;
276
277
            if (!labelElement || !textElement) {
278
                return;
279
            }
280
281
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
282
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
283
        });
284
285
        svgContainer
286
            .transition()
287
            .duration(750)
288
            .call(zoom<any, any>().on('zoom', () => zoomLayer.attr('transform', event.transform)).transform, zoomIdentity);
289
    });
290
}
291
292
export class LevelStorage {
293
    private static level: number = 1;
294
    private static maxLevel: number = 1;
295
296
    public static getLevel(): number {
297
        return this.level;
298
    }
299
300
    public static increase() {
301
        this.level = this.level + 1;
302
    }
303
304
    public static decrease() {
305
        this.level = this.level - 1;
306
    }
307
308
    public static isBelowMax() {
309
        return this.level < this.maxLevel;
310
    }
311
312
    static isAboveMin() {
313
        return this.level > 1;
314
    }
315
316
    static setMaxLevel(maxLevel: number) {
317
        this.maxLevel = maxLevel;
318
    }
319
320
    static getMaxLevel(): number {
321
        return this.maxLevel;
322
    }
323
324
    public static reset() {
325
        this.level = 1;
326
        this.maxLevel = 1;
327
    }
328
}
329